﻿package com.bizmlib.utils
{
	import com.bizmlib.utils.KeyValuePair;
	
	/**
	 * This implementation provides all of the optional map operations, and 
	 * permits null values and the null key. This class makes no guarantees 
	 * as to the order of the map; in particular, it does not guarantee that 
	 * the order will remain constant over time.
	 * 
	 * @author Valentyn Derkach
	 */
	public class Map
	{
		
		private var keyValueArray:Array;
		
		
		public function Map(...args)
		{
			this.keyValueArray = new Array();
			
			if(args.length>0)
			{
				var argumentsArr:Array = args;
				
				if(args[0] is Array)
				{
					argumentsArr = args[0] as Array;
				}
				
				for(var i:Number=0; i<argumentsArr.length; i+=2)
				{
					put(String(argumentsArr[i]), argumentsArr[i+1]);
				}
			}
		}
	
	
		public function get keys():Array
		{
			var keysList:Array = new Array();
			for(var i:Number=0; i<length; i++)
			{
				keysList.push(getKeyAt(i));
			}
			return keysList;
		}
		public function get values():Array
		{
			var valuesList:Array = new Array();
			for(var i:Number=0; i<length; i++)
			{
				valuesList.push(getValueAt(i));
			}
			return valuesList;
		}
	
		
		/**
		 * Associates the specified value with the specified key in this map. 
		 * If the map previously contained a mapping for this key, the old value 
		 * is replaced.
		 * @param key the key to put
		 * @param value the value to put with the given key
		 * @return the size of the map after the put.
		 */
		public function put(key:String, value:Object):Number
		{
			//trace(this + ".put(" + arguments + ")");
	
			// check if the key already exits, if yes, then replace
			var index:Number = indexOf(key);
			if(index != -1) {
				this.setIndexOf(index, key, value);
				return this.size();
			} else {
				return this.keyValueArray.push(new KeyValuePair(key, value));
			}
		}
	
		/**
		 * Returns value from the given index.
		 * @return the value from the given index.
		 */
		public function getKeyAt(index:Number):Object {
			return this.keyValueArray[index].getKey();
		}
		/**
		 * Returns the same value as getKeyAt does, but as a String data type. If there's no entry found by
		 * the specified index, null constant is returned then.
		 */
		public function getKeyAtAsString(index:Number):String{
			var localKey:Object = this.getKeyAt(index);
			return localKey?String(localKey):null;
		}
	
	
		/**
		 * Returns value from the given index.
		 * @return the value from the given index.
		 */
		public function getValueAt(index:Number):Object {
			return this.keyValueArray[index].getValue();
		}
		/**
		 * Returns the same value as getValueAt does, but as a String data type. If value is not defined or
		 * in case there's no any entry at the given index the return value then is a null constant.
		 */
		public function getValueAtAsString(index:Number):String{
			var localValue:Object = getValueAt(index);
			return localValue?String(localValue):null;
		}
		/**
		 * Returns the same value as getValueAt does, but as a Number data type. If value is not defined or
		 * in case there's no any entry at the given index the return value then is a null constant.
		 */
		public function getValueAtAsNumber(index:Number):Number
		{
			var localValueAsNumber:Number = getValueAt(index) as Number;
			return isNaN(localValueAsNumber) ? Number.NaN : localValueAsNumber;
		}
	
		/**
		 * Returns the number of key-value mappings in this map.
		 * @return the number of key-value mappings in this map. 
		 */
		public function size():Number {
			return this.keyValueArray.length;
		}
		public function get length():Number
		{
			return this.size();
		}
	
	
		/**
		 * Returns true if this map contains no key-value mappings.
		 * @return true if this map contains no key-value mappings.
		 */
		public function isEmpty():Boolean {
			return this.size() == 0;	
		}
	
		/**
		 * Returns the value to which the specified key is mapped in this 
		 * map, or null if the map contains no mapping for this key. 
		 * A return value of null does not necessarily indicate that the map 
		 * contains no mapping for the key; it is also possible that the map 
		 * explicitly maps the key to null.
		 * @param key
		 * @return the value for the given key, or null if value for the given 
		 * key was not found.
		 */
		public function get(key:String):Object {	
			var index:Number = indexOf(key);
			if(index > -1) {
				return this.keyValueArray[index].getValue();
			}
	
			return null;
		}
		/**
		 * Returns the same value as get does, but as a String data type. If value is not defined or key doesn't
		 * exist the return value then is a null constant.
		 */
		public function getAsString(key:String):String{
			var localValue:Object = this.get(key);
			return localValue?String(localValue):null;
		}
		/**
		 * Returns the same value as get does, but as a Number data type. If value is not defined or key doesn't
		 * exist the return value then is a null constant.
		 */
		public function getAsNumber(key:String):Number
		{
			var localValueAsNumber:Number = this.get(key) as Number;
			return isNaN(localValueAsNumber) ? Number.NaN : localValueAsNumber;
		}
	
		/**
		 * Returns true if this map contains a mapping for the specified key.
		 * @param key The key whose presence in this map is to be tested
		 * @return true if this map contains a mapping for the specified key.
		 */
		public function containsKey(key:String):Boolean {
			return this.indexOf(key) > -1;
		}
	
		/**
		 * Returns true if this map contains a mapping for the specified value 
		 * (== comparison).
		 * @param o The value whose presence in this map is to be tested
		 * @return true if this map contains a mapping for the specified value.
		 */
		public function containsValue(o:Object):Boolean {
			for(var i:Number = 0; i < this.keyValueArray.length; i++) {
				var pair:KeyValuePair = this.keyValueArray[i];
				if(pair.getValue() == o) {
					return true;
				}
			}	
			
			return false;
		}
	
		/**
		 * Returns a string representation of this map.
		 * @return the map as string.
		 */
		public function toString():String {
			//return keyValueArray.toString();
			//return "\t" + keyValueArray.join(newline + "\t");
			return "[Map	" + keyValueArray.join(", ") + "	keys:" + keys + "]";
		}
	
	
		public function removeItem(key:String):void{
			var localIndex:Number = indexOf(key);
			if(isNaN(localIndex)){return;}
			var pair:KeyValuePair = keyValueArray[localIndex];
			keyValueArray.splice(localIndex, 1);
			pair = null;
		}
	
		/**
		 * Finds the index of the given key. If the key is not found, then returns -1
		 */
		private function indexOf(key:String):Number {
			for(var i:Number = 0; i < this.keyValueArray.length; i++) {
				var pair:KeyValuePair = this.keyValueArray[i];
				if(pair.getKey() == key) {
					return i;	
				}
			}
			
			return -1;
		}
	
		/**
		 * Sets the given key value pair to the given index.
		 */
		private function setIndexOf(index:Number, key:String, value:Object):void {
			this.keyValueArray[index] = new KeyValuePair(key, value);
		}
	
	
		public function clone():Map
		{
			//trace(this + ".clone(" + arguments + ")");
			var arrCopy:Array = new Array();
			for(var i:Number=0; i<keyValueArray.length; i++)
			{
				arrCopy.push(keyValueArray[i].getKey());
				arrCopy.push(keyValueArray[i].getValue());
				//arrCopy[i] = KeyValuePair(arrCopy[i]).clone();
			}
			return new Map(arrCopy);
		}
	}
}